//
//  Networking.swift
//  Entrance
//
//  Created by chunhuiLai on 2018/1/12.
//  Copyright © 2018年 chunhuiLai. All rights reserved.
//

import Foundation
import Moya
import Alamofire
import RxSwift

class OnlineProvider<Target> where Target: Moya.TargetType {
    
    fileprivate let online: Observable<Bool>
    fileprivate let provider: MoyaProvider<Target>
    
    init(endpointClosure: @escaping MoyaProvider<Target>.EndpointClosure = MoyaProvider.defaultEndpointMapping,
         requestClosure: @escaping MoyaProvider<Target>.RequestClosure = MoyaProvider.defaultRequestMapping,
         stubClosure: @escaping MoyaProvider<Target>.StubClosure = MoyaProvider.neverStub,
         manager: Manager = MoyaProvider<Target>.defaultAlamofireManager(),
         plugins: [PluginType] = [],
         trackInflights: Bool = false,
         online: Observable<Bool> = connectedToInternetOrStubbing()) {
        
        self.online = online
        self.provider = MoyaProvider(endpointClosure: endpointClosure, requestClosure: requestClosure, stubClosure: stubClosure, manager: manager, plugins: plugins, trackInflights: trackInflights)
    }
    
    func request(_ target: Target) -> Observable<Moya.Response> {
        let actualRequest = provider.rx.request(target)
        return online
            .ignore(value: false)  // Wait until we're online
            .take(1)        // Take 1 to make sure we only invoke the API once.
            .flatMap { _ in // Turn the online state into a network request
                return actualRequest
        }
    }
}

protocol NetworkingType {
    associatedtype T: TargetType
    var provider: OnlineProvider<T> { get }
}


struct Networking: NetworkingType {
    typealias T = SYInfinityAPI
    let provider: OnlineProvider<SYInfinityAPI>
}

private extension Networking {
    
    /// Request to fetch and store new token if the current token is missing or expired.
    func tokenRequest(_ defaults: UserDefaults) -> Observable<String?> {
        
        var appToken = SYToken()
        
        // If we have a valid token, return it and forgo a request for a fresh one.
        if appToken.isValid {
            return Observable.just(appToken.token)
        }
        
        let newTokenRequest = self.provider.request(SYInfinityAPI.zen)
            .filterSuccessfulStatusCodes()
            .mapJSON()
            .map { element -> (token: String?, expiry: String?) in
                guard let dictionary = element as? NSDictionary else { return (token: nil, expiry: nil) }
                
                return (token: dictionary["xapp_token"] as? String, expiry: dictionary["expires_in"] as? String)
            }
            .do(onNext: { element in
                // These two lines set the defaults values injected into appToken
                appToken.token = element.0
                //appToken.expiry = KioskDateFormatter.fromString(element.1 ?? "")
            })
            .map { (token, expiry) -> String? in
                return token
            }
            .logError()
        
        return newTokenRequest
    }
}

// "Public" interfaces
extension Networking {
    /// Request to fetch a given target. Ensures that valid XApp tokens exist before making request
    func request(_ target: SYInfinityAPI, defaults: UserDefaults = UserDefaults.standard) -> Observable<Moya.Response> {
        
        let actualRequest = self.provider.request(target)
        return self.tokenRequest(defaults).flatMap { _ in actualRequest }
    }
}


// Static methods
extension NetworkingType {
    
    static func newDefaultNetworking() -> Networking {
        return Networking(provider: newProvider(plugins))
    }
    
    static func newStubbingNetworking() -> Networking {
        return Networking(provider: OnlineProvider(endpointClosure: endpointsClosure(), requestClosure: Networking.endpointResolver(), stubClosure: MoyaProvider.immediatelyStub, online: .just(true)))
    }

    static func endpointsClosure<T>(_ accessToken: String? = nil) -> (T) -> Endpoint<T> where T: TargetType {
        return { target in
            let endpoint: Endpoint<T> = Endpoint<T>(url: target.urlString, sampleResponseClosure: {.networkResponse(200, target.sampleData)}, method: target.method, task: target.task, httpHeaderFields: ["X-Access-Token": accessToken ?? ""])
            
            return endpoint
        }
    }
    
    static func APIKeysBasedStubBehaviour<T>(_: T) -> Moya.StubBehavior {
        return .immediate
    }

    static var plugins: [PluginType] {
        return [
            NetworkLogger(blacklist: { target -> Bool in
                guard let target = target as? SYInfinityAPI else { return false }
                
                switch target {
                case .zen: return true
                default: return false
                }
            })
        ]
    }

    // (Endpoint<Target>, NSURLRequest -> Void) -> Void
    static func endpointResolver<T>() -> MoyaProvider<T>.RequestClosure where T: TargetType {
        return { (endpoint, closure) in
            var request = try! endpoint.urlRequest() 
            request.httpShouldHandleCookies = false
            closure(.success(request))
        }
    }
}

private func newProvider<T>(_ plugins: [PluginType], accessToken: String? = nil) -> OnlineProvider<T> {
    return OnlineProvider(endpointClosure: Networking.endpointsClosure(accessToken),
                          requestClosure: Networking.endpointResolver(),
                          stubClosure: Networking.APIKeysBasedStubBehaviour,
                          plugins: plugins)
}

